package ca.usask.cs.decisiontree;

import java.util.HashMap;
import java.util.Iterator;

import javax.swing.JOptionPane;

import ca.usask.cs.decisiontree.api.DecisionTree;
import ca.usask.cs.decisiontree.api.EventNodeInterface;
import ca.usask.cs.decisiontree.api.Edge;
import ca.usask.cs.decisiontree.api.IntermediateTreeNode;
import ca.usask.cs.decisiontree.api.Node;
import ca.usask.cs.exceptions.Exceptions;

public class TreeStructureHandler {
	private BasicDecisionTree tree;
	private Node rootNode;
	private Edge currentEdge;
	private Node currentNode;
	private HashMap IDList;
	private int nextID;
	private Exceptions exception;
	
	public TreeStructureHandler(){
		this.tree = null;
        this.rootNode = null;
        this.currentEdge = null;
        this.currentNode = null;
        this.nextID = 0;
        this.IDList = new HashMap();
        this.exception = new Exceptions();
	}
	
	 /**
     * adds the newID into the hashmap, the key and the value is both the newID
     * throws IllegalArgumentException if the ID already exists
     * @param newID
     */
    public void addID(int newID) throws IllegalArgumentException
    {
        if(hasID(newID))
        {
            throw new IllegalArgumentException("The ID being added already exists.");
        }
        this.IDList.put(newID, newID);
    }
    
    /**
     * determines if the IDList contains theID
     * @param theID id in int
     * @return true if theID is in use
     *          false if it is not in used
     */
    public boolean hasID(int theID)
    {
        return this.IDList.containsKey(theID);
    }
    
    /**
     * Generates the next ID. If the ID already exists, it will generate a new ID
     * Places the new ID into the keys IDList
     * @return
     */
    protected int getNextID()
    {
        nextID++;
        // increment the ID until it is not in the list
        while(hasID(nextID))
        {
            nextID++;
        }
        //add the nextID onto the list
        addID(nextID);
        return nextID;
    }
    
    /**
     * removes the removeID from the IDlist
     * Does nothing if it does not exist
     * @param removeID in int
     */
    public void removeID(int removeID)
    {
        this.IDList.remove(removeID);
    }
    /**
     * Returns the current Node
     * @return
     */
    public Node getCurrentNode()
    {
        return this.currentNode;
    }

    /**
     * Returns the current Edge
     * @return
     */
    public Edge getCurrentEdge()
    {
        return this.currentEdge;
    }

    /**
     * Returns the RootNode
     * @return
     */
    public Node getRootNode()
    {
        return this.rootNode;
    }
    
    /**
     * Returns the Tree
     * @return
     */
    public DecisionTree getDecisionTree()
    {
        return this.tree;
    }
   
    public BasicDecisionTree getBasicDecisionTree()
    {
        return this.tree;
    }
   
   public void initTree(){
	   cleanup();
   }
   
   public void cleanup(){
	   this.tree = null;
       this.rootNode = null;
       this.currentEdge = null;
       this.currentNode = null;
       this.nextID = 0;
       this.IDList.clear();
   }
   
   
   
   /**
    * creates a basic decision tree with the given rootNode
    * throws IllegalArgumentException
       * if the given root node is a terminal node
       * if the given root node has a source edge
    *
    */
   public BasicDecisionTree createBasicDecisionTree(Node theRootNode)
   {
	   try{
		   exception.nullNodeCheck(theRootNode,"The Root Node cannot be null");
		   theRootNode.notTerminalNodeCheck("The Root Node cannot be a terminal node");
	       exception.rootNodeHasSourceEdgeCheck(theRootNode,"The Root Node cannot have a source Edge");
	   }
	   catch(IllegalArgumentException e){
			System.out.println(e.getMessage());
		}

       tree = new BasicDecisionTree(theRootNode, getNextID());
       this.currentNode = theRootNode;
       this.rootNode = theRootNode;

       return tree;
   }
   
   /**
    * creates a basic decision tree with the given rootNode
    * throws IllegalArgumentException
       * if the given root node is a terminal node
       * if the given root node has a source edge
    *
    */
   public BasicDecisionTree createBasicDecisionTree(Node theRootNode, int treeID) throws IllegalArgumentException
   {
	   try{
		   exception.nullNodeCheck(theRootNode,"The Root Node cannot be null");
	       theRootNode.notTerminalNodeCheck("The Root Node cannot be a terminal node");
	       exception.rootNodeHasSourceEdgeCheck(theRootNode,"The Root Node cannot have a source Edge");
	   }
	   	catch(IllegalArgumentException e){
			System.out.println(e.getMessage());
		}
       
       if(hasID(treeID))
       {
           throw new IllegalArgumentException("The given treeID has already been taken");
       }

       tree = new BasicDecisionTree(theRootNode, treeID);
       this.currentNode = theRootNode;
       this.rootNode = theRootNode;

       return tree;
   }
   /**
    * creates a new Edge with the specified source and destination node
    *
    * Throw IllegalArgumentException if Terminal Node is a source node
    * 
    * @param sourceNode - source Node of the Edge
    * @param destinationNode - destination node of the Edge
    */
   public BasicEdge createBasicEdge(Node sourceNode, Node destinationNode,String name) throws IllegalArgumentException
   {
	   try{
		   sourceNode.DecisionNodeCheck();
		   exception.nodeEqualCheck(this.rootNode, destinationNode,"The destination node cannot be the root node");     
	   }
	   	catch(IllegalArgumentException e){
			System.out.println(e.getMessage());
		}
	   BasicEdge newEdge = new BasicEdge(sourceNode, destinationNode, getNextID(),name);
       return newEdge;
       
   }
   
   /**
    * creates a new Edge with the specified source and destination node
        * and edgeID.
        * Addes the edgeID to IDList
        * Throw IllegalArgumentException
        * if Terminal Node is a source node
        * if the edgeID is already in use
    *
    * @param sourceNode - source Node of the Edge
    * @param destinationNode - destination node of the Edge
    */
   public BasicEdge createBasicEdge(Node sourceNode, Node destinationNode, int edgeID,String name) throws IllegalArgumentException
   {
	   try{
		   sourceNode.DecisionNodeCheck();
		   exception.nodeEqualCheck(this.rootNode, destinationNode,"The destination node cannot be the root node");
	   }
	   catch(IllegalArgumentException e){
   			System.out.println(e.getMessage());
   	   }
       BasicEdge newEdge = new BasicEdge(sourceNode, destinationNode, edgeID,name);
       addID(edgeID);
       return newEdge;

   }

   /**
    * creates a new Edge with the specified source and destination node
    *
    * Throw IllegalArgumentException if Terminal Node is a source node
    *
    * @param sourceNode - source Node of the Edge
    * @param destinationNode - destination node of the Edge
    */
   public EventEdge createChanceEdge(Node sourceNode, Node destinationNode,String name) throws IllegalArgumentException
   {
	   try{
	      sourceNode.ChanceNodeCheck("Source Node of a ChanceEdge must be a ChanceNode");
	      exception.nodeEqualCheck(this.rootNode, destinationNode,"The destination node cannot be the root node");
	   }
	   	catch(IllegalArgumentException e){
    		System.out.println(e.getMessage());
    	}
       EventEdge newEdge = new EventEdge(sourceNode, destinationNode, getNextID(),name);
       return newEdge;

   }
   
   /**
    * creates a new Edge with the specified source and destination node
        * and edgeID.
        * Addes the edgeID to IDList
    * Throw IllegalArgumentException
        * if Terminal Node is a source node
        * if the edgeID is already in use
    *
    * @param sourceNode - source Node of the Edge
    * @param destinationNode - destination node of the Edge
    */
   public EventEdge createChanceEdge(Node sourceNode, Node destinationNode, int edgeID, String name) throws IllegalArgumentException
   {
	   try{
		   sourceNode.ChanceNodeCheck("Source Node of a ChanceEdge must be a ChanceNode");
	
	       if(hasID(edgeID))
	       {
	
	           throw new IllegalArgumentException("The given edgeID has already been taken");
	       }
	       exception.nodeEqualCheck(this.rootNode, destinationNode,"The destination node cannot be the root node");
	   }
	   	catch(IllegalArgumentException e){
    		System.out.println(e.getMessage());
    	}
       EventEdge newEdge = new EventEdge(sourceNode, destinationNode, edgeID, name);
       addID(edgeID);
       return newEdge;

   }
   /**
    * creates a new Decision Node
    * @return
    */
   public DecisionNode createDecisionNode()
   {
       DecisionNode newDecisionNode = new DecisionNode(getNextID());
       return newDecisionNode;
   }

   /**
    * creates a new Decision Node with given treeNodeID
        * Addes the edgeID to IDList
    * @return
    */
   public DecisionNode createDecisionNode(int treeNodeID) throws IllegalArgumentException
   {
       if(hasID(treeNodeID))
       {
           throw new IllegalArgumentException("The given treeNodeID has already been taken");
       }

       DecisionNode newDecisionNode = new DecisionNode(treeNodeID);
       addID(treeNodeID);
       return newDecisionNode;
   }

   /**
    * creates a new Chance Node without a given probability. The probability
    * will be set to 0.0
    * @return
    */
   public EventNode createChanceNode()
   {
       EventNode newChanceNode = new EventNode(getNextID());
       return newChanceNode;
   }

   /**
    * creates a new Chance Node without a given probability with given
    * treeNodeID. The probability will be set to 0.0
        * Addes the edgeID to IDList
    * @return
    */
   public EventNode createChanceNode(int treeNodeID) throws IllegalArgumentException
   {
       if(hasID(treeNodeID))
       {
           throw new IllegalArgumentException("The given treeNodeID has already been taken");
       }
       EventNode newChanceNode = new EventNode(treeNodeID);
       addID(treeNodeID);
       return newChanceNode;
   }

   /**
    * creates a new Terminal Node
    * @return
    */
   public TerminalNode createTerminalNode()
   {
       TerminalNode newTerminalNode = new TerminalNode(getNextID());
       return newTerminalNode;
   }
   /**
    * Add newEdge with a specified probability for a chance node
    * Throw UnsupportedOperationException if the current node is not a chance node
    * @param newEdge
    * @return true on success
    *         false on error
    */
   public void addChildEdge(Edge newEdge, double newProbability) throws  UnsupportedOperationException
   {
       // throw unsupportedOperationException if current node not a chance node
	   try{
		   this.currentNode.ChanceNodeCheck("Terminal Nodes cannot have add a child edge");
		   exception.nullNodeCheck(this.currentNode,"There current node is Null");
	   }
	   	catch(IllegalArgumentException e){
    		System.out.println(e.getMessage());
    	}

       EventNodeInterface tempChanceNode;
       tempChanceNode = (EventNodeInterface)this.currentNode;
       tempChanceNode.addChildEdge(newEdge, newProbability);
   }

   /**
    * Retrieves an iterator of Edges
    * throw UnsupportedOperationException 
    *         -if Terminal is current
    *         -if current is null
    * else cast to correct node and call getChildEdgeList
    * @return iterator of the linked list
    */
   public Iterator<Edge> getChildEdgeList() throws UnsupportedOperationException
   {
	   try{
		   exception.nullNodeCheck(this.currentNode,"Terminal Node cannot have children");       
	       this.currentNode.notTerminalNodeCheck("Terminal Node cannot have children");
	   }
	   	catch(IllegalArgumentException e){
    		System.out.println(e.getMessage());
    	}

       IntermediateTreeNode tempIntermediateNode;
       tempIntermediateNode = (IntermediateTreeNode)this.currentNode;
       return tempIntermediateNode.getChildEdgeList();
       
   }

    /**
    * Remove removeEdge from the current node
    * if the child edge does not exist in the current Node, nothing happens.
        * throw UnsupportedOperationException
    *         -if Terminal is current
    *         -if current is null
    * else cast to correct node and call removeChildEdge
    * @param removeEdge
    * @return true on success
    *         false on error
    */
   public boolean removeChildEdge(Edge removeEdge) throws UnsupportedOperationException
   {
	   try{
		   exception.nullNodeCheck(this.currentNode,"Terminal Node cannot have children");
	       this.currentNode.notTerminalNodeCheck("Terminal Node cannot have children");
	   }
	   	catch(IllegalArgumentException e){
			System.out.println(e.getMessage());
		}

       IntermediateTreeNode tempIntermediateNode;
       tempIntermediateNode = (IntermediateTreeNode)this.currentNode;
       return tempIntermediateNode.removeChildEdge(removeEdge);
   }

   /**
    * Retrieves the source edge of the current Node
    * throw UnsupportedOperationException if current is root Node or null
    * @return
    */
   public Edge getSourceEdge() throws UnsupportedOperationException
   {
	   try{
		   exception.nullNodeCheck(this.currentNode,"The current node is null");
	   }
	   catch(IllegalArgumentException e){
			System.out.println(e.getMessage());
		}

       return this.currentNode.getSourceEdge();

   }
   /**
    * Set the source Edge of the current Node
    * UnsupportedOperationException if the current node is a root node or null
    * @param sourceEdge
    */
   public void setSourceEdge(Edge sourceEdge) throws UnsupportedOperationException, IllegalArgumentException
   {
	   try{
		   exception.nodeEqualCheck(this.currentNode, this.rootNode,"The Root Node cannot have a source Edge");
		   exception.nullNodeCheck(this.currentNode, "The current node is null");
	   }
	   	catch(IllegalArgumentException e){
			System.out.println(e.getMessage());
		}

       this.currentNode.setSourceEdge(sourceEdge);
   }

   /**
    * Set the source Node of the current Node
    * throw UnsupportedOperationException if current edge is null
    *                                     if the current edge is chanceEdge,
    *                                      the sourceNode must be ChanceNode
    * throw illegalArgumentException if the sourcenode is a terminal node
    * @param sourceNode
    */
   public void setSourceNode(Node sourceNode) throws UnsupportedOperationException, IllegalArgumentException
   {
	   try{
		   exception.nullEdgeCheck(this.currentEdge, "The current edge is null");
	       this.currentEdge.typeCheck(sourceNode);
	       sourceNode.notTerminalNodeCheck("The SourceNode cannot be a terminal Node");
	       this.currentEdge.setSourceNode(sourceNode);
	   }
	   catch(IllegalArgumentException e){
			System.out.println(e.getMessage());
		}

   }

   /**
    * Set the Destination node of the current Node
    * throw UnsupportedOperationException if destinationNode is null
    * throw IllegalArgumentException if destinationNode is the root node
    * @param destinationNode
    */
   public void setDestinationNode(Node destinationNode)throws UnsupportedOperationException, IllegalArgumentException
   {
	   try{
		   exception.nullEdgeCheck(this.currentEdge, "The current edge is null");
		   exception.nodeEqualCheck(this.rootNode, destinationNode,"The destination node cannot be the root node");
	   }
	   catch(IllegalArgumentException e){
			System.out.println(e.getMessage());
		}

       this.currentEdge.setSourceNode(destinationNode);
   }

   /**
    * sets theCurrentEdge to the new current Edge
    *      - unselect the current Edge
    *      - select the new Edge
    *
    * @param theCurrentEdge
    */
   public void setCurrentEdge(Edge theCurrentEdge)
   {
       this.currentEdge = theCurrentEdge;
   }

       /**
    * sets theCurrentNode to the new current Node
        * - unselcect the current Node
        * - select the new Node
    * @param theCurrentNode
    */
   public void setCurrentNode(Node theCurrentNode)
   {
       this.currentNode = theCurrentNode;
   }

   /**
    * set the edge with the given ID as the current edge
       * if there is no edge with the specified ID, no changes will be made
    * calls setCurrenEdgeHelper to perform dfs to find the edge with the
    * specified ID
    * @param id - the id of the new current edge
    */
   public void setCurrentEdge(int id)
   {
       if(this.rootNode != null)
       {
           setCurrentEdgeHelper(this.rootNode, id);
       }
   }
   /**
    * recursive function that searches for an edge with a specified ID
    *  uses dfs
    * Base Case:   curNode is terminal Node
    * Match:       if a child edge's id is the same as the given id
    * Recurse:     loop through each of the destination Node of the child
    *
    * Assumption:  there are no duplicate id's in the tree.
    * @param curNode
    */
   protected void setCurrentEdgeHelper(Node curNode, int id)
   {
       // if this is a terminalNode, its a base case and r
       if(curNode instanceof TerminalNode)
       {
    	   
           return;
       }
       // else it must be an intermeidateTreeNode.
       // get its child list and loop through each child, check if the edge is
       // the targeted edge. if so, set set as current and end.
       // if not, recurse to its destination node.
       else if(curNode instanceof EventNode ||curNode instanceof DecisionNode)
       {
           IntermediateTreeNode curInter = (IntermediateTreeNode) curNode;
           Iterator<Edge> edgeList = curInter.getChildEdgeList();

           // loop through each edge
           // the edge is the match, return, else, recurse with its destination node
           while(edgeList.hasNext())
           {
               Edge curEdge = edgeList.next();
               if(curEdge.getEdgeID() == id)
               {
                   this.currentEdge = curEdge;
                   return;
               }
               else
               {
                   this.setCurrentEdgeHelper(curEdge.getDestinationNode(), id);
               }
           }
       }
   }
   /**
    * creates a new Terminal Node with the specified treeNodeID
        * Addes the edgeID to IDList
    * @return
    */
   public TerminalNode createTerminalNode(int treeNodeID) throws IllegalArgumentException
   {
       if(hasID(treeNodeID))
       {
    	   JOptionPane.showMessageDialog(null,"The given treeNodeID: " + treeNodeID + " has already been taken");
           throw new IllegalArgumentException("The given treeNodeID: " + treeNodeID + " has already been taken");
       }
       TerminalNode newTerminalNode = new TerminalNode(treeNodeID);
       addID(treeNodeID);
       return newTerminalNode;
   }

   /**
    * Add newEdge to the current Node
    * Throw UnsupportedOperationException if the current node is a terminal 
    * node or null
    * @param newEdge
    * @return true on success
    *         false on error
    */
   public void addChildEdge(Edge newEdge) throws UnsupportedOperationException
   {

       // throw unsupportedOperationException if terminal node attempts to add child
	   try{
		   this.currentNode.notTerminalNodeCheck("Terminal Nodes cannot have add a child edge"); 
		   exception.nullNodeCheck(this.currentNode, "The current node is null");
	   }
	   catch(IllegalArgumentException e){
			System.out.println(e.getMessage());
		}

       IntermediateTreeNode tempIntermediateNode;
       tempIntermediateNode = (IntermediateTreeNode)this.currentNode;
       tempIntermediateNode.addChildEdge(newEdge);
       
       
   }
   /**
    * set the edge with the given ID as the current node
       * if there is no node with the specified ID, no changes will be made
    * calls setCurrenNodeHelper to perform dfs to find the node with the
    * specified ID
    * @param id - the id of the new current edge
    */
   public void setCurrentNode(int id)
   {
       if(this.rootNode != null)
       {
           setCurrentNodeHelper(this.rootNode, id);
       }
   }


   /**
    * recursive function that searches for an edge with a specified ID
    *  uses dfs
    * Base Case:   curNode is terminal Node
    * Match:       if te curNod's id is the same as the given id
    * Recurse:     loop through each of the destination Node of the child
    *
    * Assumption:  there are no duplicate id's in the tree.
    * @param curNode
    */
   protected void setCurrentNodeHelper(Node curNode, int id)
   {
       // if the curNode's ID is the match, set as currentNode and return;
       if(curNode.getTreeNodeID() == id)
       {
           this.currentNode = curNode;
           return;
       }

       // if this is a terminalNode, its a base case and r
       else if(curNode instanceof TerminalNode)
       {
           return;
       }
       // else it must be an intermeidateTreeNode.
       // get its child list and loop through each child, check if the edge is
       // the targeted edge. if so, set set as current and end.
       // if not, recurse to its destination node.
       else if(curNode instanceof EventNode ||curNode instanceof DecisionNode)
       {
           IntermediateTreeNode curInter = (IntermediateTreeNode) curNode;
           Iterator<Edge> edgeList = curInter.getChildEdgeList();

           // loop through each edge
           // the edge is the match, return, else, recurse with its destination node
           while(edgeList.hasNext())
           {
               Edge curEdge = edgeList.next();
               this.setCurrentNodeHelper(curEdge.getDestinationNode(), id);
           }
       }
   }
}
